package org.testfire.realm.control.user;

import com.lookingglasssoftware.lgsrealm.ChallengeServer;
import com.lookingglasssoftware.lgsrealm.LGSChecksum;
import javax.annotation.Resource;
import javax.faces.context.FacesContext;
import javax.faces.event.ActionEvent;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.transaction.UserTransaction;
import org.apache.log4j.Logger;
import org.testfire.realm.control.auth.user.UserDBAO;
import org.testfire.realm.control.exception.UserUpdateException;
import org.testfire.realm.model.entities.User;

/**
 * Methods which will be needed by many different classes involving m_user interaction, such as:
 * - Registration
 * - Changing Account Settings
 * - Relating a m_user to a site addition (e.g. m_user posts to a message board, etc).
 *
 * @revisit This code could use a detailed lookover. I would imagine it (and the entire user tree) could use
 * re-writing.
 */
public abstract class AbstractUserBean
{
    Logger log = Logger.getLogger( AbstractUserBean.class);

    /**
     * Get the persistence unit which has access to the Users database schema (lgsrealm).
     */
    @PersistenceContext(unitName = "lgsrealm")
    EntityManager em;

    /**
     * <p/>
     * The UserTransaction injection is not available to classes outside of the persistence
     * context unless they are managed and managed as request-context beans.
     * </p><p>
     * We need the transaction so that we may wrap any updates that a m_user does to their
     * profile (and to allow us to create new users).
     * </p>
     */
    @Resource
    private UserTransaction utx;

    /**
     * Store the m_user once we get them out of the DB, so that we don't have to keep calling
     * the DB.
     */
    User m_user;

    /**
     * Store the UserDBAO once we create one via the Application Context, so that we don't
     * have to keep creating them.
     */
    UserDBAO m_dbao;

    /**
     * Default Constructor.
     */
    public AbstractUserBean()
    {
    }

    /**
     * Get a challenge from the Realm Challenge Server.
     * This is used on the login page to help us secure an otherwise non-secure login page.
     * More information is availble in the LGSRealm docs.
     *
     * @return A challenge code for use in an HMAC MD5.
     */
    public String getChallenge()
    {
        return ChallengeServer.getInstance().newChallenge();
    }

    /**
     * Get the remote m_user currently present on the externalContext.
     * Once the m_user is retrieved via a getRemoteUser() request of the external context
     * it is kept in memory.
     * Because this class is stored out of the persistence context we
     * must declare this bean (or rather, classes that extend it) as a request-context bean
     * in the faces-config file.
     *
     * @return The remote m_user currently present on the externalContext.
     */
    public User getUser()
    {
        if ( m_user == null )
        {
            try
            {
                m_user = dbao().getCurrentUser();
            }
            catch ( Throwable ex )
            {
                log.error("",ex );
                return null;
            }
        }
        return m_user;
    }

    /**
     * Get the name of the remote m_user currently present on the externalContext.
     *
     * @return The name of the remote m_user currently present on the externalContext.
     */
    public String userName()
    {
        return context().getExternalContext().getRemoteUser();
    }

    /**
     * Handle the m_user logging out.
     * By taking an ActionEvent, we allow this method to be used as an
     * action listener in our JavaServerFaces files.
     * We set a command link to use this method as the actionListener:
     * {h:commandLink id="logout" action="nav_logout" actionListener="#{UserBean.userLogout}"}<br>
     * {h:outputText value="Log Out |"/}<br>
     * {/h:commandLink} <br>
     *
     * @param actionEvent Passed by the JavaServerFaces instance when called as an action.
     */
    public void userLogout( ActionEvent actionEvent )
    {
        FacesContext context = FacesContext.getCurrentInstance();

        // external context
        Object obj = context.getExternalContext().getRequest();
        HttpServletRequest r = (HttpServletRequest)obj;
        HttpSession s = r.getSession( false );
        if ( s != null )
        {
            s.invalidate();
        }
        log.info( "User '" + userName() + "' Logged out. Actionevent: "
            + (actionEvent != null ? actionEvent.toString() : ""));
    }

    /**
     * <p>Return the <code>FacesContext</code> instance for the
     * current request.
     * A convenience method.
     *
     * @return The <code>FacesContext</code> instance for the
     *         current request.
     */
    protected FacesContext context()
    {
        return FacesContext.getCurrentInstance();
    }

    /**
     * Return the database access object (DBAO) for our database.
     *
     * @return UserDBAO The database access object giving us access to m_user data.
     */
    protected UserDBAO dbao()
    {
        if ( m_dbao == null )
        {
            m_dbao = UserDBAO.getInstance();
        }
        return m_dbao;
    }

    /**
     * A convinience method which is equivelant to the call 'updateUserEntity(true)'.
     *
     * @throws UserUpdateException If the User entity could not be updated. Could be a
     *                             database connectivity error, or perhaps the SQL was invalid, etc.
     */
    public void updateUserEntity() throws UserUpdateException
    {
        updateUserEntity( true );
    }

    /**
     * Update the data which has changed on the UserEntity. This operation pushes any changes
     * made to the m_user entity bean back into the database.
     * <p/>
     * The entire operation is wrapped in a transaction, so any failure will result in
     * all the changes to be rolled back.<br>
     * This won't change the values on the entity, it will simply prevent any merge
     * from being made to the underlying database.
     *
     * @param testName If true, check that the current m_user is the same as the m_user whose
     *                 data is about to be updated.
     * @throws UserUpdateException If the User entity could not be updated. Could be a
     *                             database connectivity error, or perhaps the SQL was invalid, etc.
     */
    protected void updateUserEntity( boolean testName ) throws UserUpdateException
    {
        log.info( "AbstractUserBean.updateUserEntity" );
        // sanity check
        if ( testName && !getUser().getName().equals( userName() ) )
        {
            throw new UserUpdateException(
                "Security violation: current m_user is attempting to commit a change on a m_user other than self: "
                    + getUser().getName() + " != " + userName() );
        }
        try
        {
            utx.begin(); // this starts a transaction
            dbao().update( getUser() ); // get the m_user and merge them via the UserDBAO.
            utx.commit(); // if the merge succeeded, we commit the whole operation....
        }
        catch ( Exception ex )
        { // If the udpate failed, we get an exception...
            log.error("",ex );
            try
            {
                utx.rollback(); // ... and we can rollback any changes that were made.
            }
            catch ( Exception exe )
            {
                log.error("",exe );
                /**
                 * could not roll the transaction back. Probably a null pointer on the UserTransaction.
                 * Check that the ManagedBean which is calling this class is set to Session-Scope
                 * in the faces-config.xml file. Like so:
                 *
                 * <managed-bean>
                 *     <managed-bean-name>UserBean</managed-bean-name>
                 *     <managed-bean-class>org.testfire.realm.control.m_user.UserBean</managed-bean-class>
                 *     <managed-bean-scope>request</managed-bean-scope>
                 * </managed-bean>
                 */
                throw new UserUpdateException( ex.getMessage() );
            }
            throw new UserUpdateException( "Could not merge m_user entity: " + ex.getMessage() );
        }
        log.info( "Commited Changes on m_user '" + userName() + "'." );
    }

    /**
     * Create and return a random confirmation code.
     * This code is used to create an URL to e-mail to the m_user. The value
     * will be stored in the users record, allowing us to confirm the m_user's account
     * when they follow the link sent to them.
     * <p/>
     * The confirmation code doesn't need to be highly random, just random enough that
     * nobody will be able to fake them to create a bunch of confirmed spam accounts.
     *
     * @return A random confirmation code.
     */
    public static String createConfirmCode()
    {
        String code = String.valueOf( Math.random() * System.currentTimeMillis() );
        return LGSChecksum.getMD5( code );
    }

}
